Typed JSON
Typed JSON provides a json!
macro to build an impl serde::Serialize
type with very natural JSON syntax.
use typed_json::json;
let john = json!({
"name": "John Doe",
"age": 43,
"phones": [
"+44 1234567",
"+44 2345678"
]
});
println!("{}", serde_json::to_string(&john).unwrap());
One neat thing about the json!
macro is that variables and expressions can
be interpolated directly into the JSON value as you are building it. Serde
will check at compile time that the value you are interpolating is able to
be represented as JSON.
let full_name = "John Doe";
let age_last_year = 42;
fn random_phone() -> String {
"0".to_owned()
}
let john = typed_json::json!({
"name": full_name,
"age": age_last_year + 1,
"phones": [
format!("+44 {}", random_phone())
]
});
Comparison to serde_json
This crate provides a typed version of serde_json::json!()
.
What does that mean? It means it performs 0 allocations and it creates a custom type for the JSON object you are representing.
For one-off JSON documents, this ends up being considerably faster to encode.
This is 100% compatible with serde_json::json!()
syntax as of serde_json = "1.0.108"
.
Benchmark
The following benchmarks indicate serializing a complex deeply-nested JSON document to a String
.
Note: the typed_json_core
benchmark uses serde-json-core
to encode to a heapless::String
.
Timer precision: 41 ns
serialize_string fastest │ slowest │ median │ mean │ samples │ iters
├─ serde_json 765.3 ns │ 15.1 µs │ 807 ns │ 824.9 ns │ 100000 │ 800000
├─ typed_json 148.1 ns │ 1.606 µs │ 153.3 ns │ 156 ns │ 100000 │ 3200000
╰─ typed_json_core 217.1 ns │ 2.991 µs │ 228.8 ns │ 240.4 ns │ 100000 │ 3200000
Note: The benchmarks use serde_json::to_string
as it's significantly faster than the ToString
/Display
implementation, both for serde_json::json
and typed_json::json
No-std support
It is possible to use typed_json
with only core
. Disable the default "std"
feature:
[dependencies]
typed_json = { version = "0.1", default-features = false }
To encode the Serialize
type to JSON:
you will either need serde_json
with the alloc
feature
[dependencies]
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
or serde-json-core
with no dependency on alloc
[dependencies]
serde-json-core = "0.5.1"
How it works
Note: all of this is implementation detail and none of this is stable API
let data = json!({
"codes": [400, value1, value2],
"message": value3,
"contact": "contact support at support@example.com"
});
Expands into something like
let data = typed_json::__private::Map(hlist![
typed_json::__private::KV::Pair(
typed_json::__private::Expr("codes"),
typed_json::__private::Array(hlist![
typed_json::__private::Expr(400),
typed_json::__private::Expr(value1),
typed_json::__private::Expr(value2),
]),
),
typed_json::__private::KV::Pair(
typed_json::__private::Expr("message"),
typed_json::__private::Expr(value3)
),
typed_json::__private::KV::Pair(
typed_json::__private::Expr("contact"),
typed_json::__private::Expr("contact support at support@example.com")
),
]);
where hlist![a, b, c, d, e]
would expand into
(a, ((b, c), (d, e)))
Compile time benchmarks
There's no such thing as a true zero-cost abstraction. However, it seems that sometimes
typed-json
compiles faster than serde_json
, and sometimes the opposite is true.
I measured the compile times using the large service JSON from https://kubernetesjsonschema.dev/.
Many small documents
In this test, I have split the above JSON file into 31 reasonably-sized documents
Debug
$ hyperfine \
--command-name "typed_json" \
"pushd tests/crates/stress3 && touch src/main.rs && cargo build" \
--command-name "serde_json" \
"pushd tests/crates/stress4 && touch src/main.rs && cargo build"
Benchmark 1: typed_json
Time (mean ± σ): 148.6 ms ± 3.7 ms [User: 141.2 ms, System: 82.0 ms]
Range (min … max): 143.3 ms … 157.0 ms 20 runs
Benchmark 2: serde_json
Time (mean ± σ): 151.7 ms ± 4.8 ms [User: 134.9 ms, System: 98.5 ms]
Range (min … max): 143.2 ms … 163.0 ms 20 runs
Summary
typed_json ran
1.02 ± 0.04 times faster than serde_json
Release
$ hyperfine \
--command-name "typed_json" \
"pushd tests/crates/stress3 && touch src/main.rs && cargo build --release" \
--command-name "serde_json" \
"pushd tests/crates/stress4 && touch src/main.rs && cargo build --release"
Benchmark 1: typed_json
Time (mean ± σ): 538.3 ms ± 7.1 ms [User: 877.5 ms, System: 65.7 ms]
Range (min … max): 527.4 ms … 550.9 ms 10 runs
Benchmark 2: serde_json
Time (mean ± σ): 1.003 s ± 0.013 s [User: 1.194 s, System: 0.075 s]
Range (min … max): 0.972 s … 1.020 s 10 runs
Summary
typed_json ran
1.86 ± 0.04 times faster than serde_json
One-off large document
In this test, I have included the single JSON file in verbatim.
I don't think this is a realistic use case but still interesting
Debug
$ hyperfine \
--command-name "typed_json" \
"pushd tests/crates/stress1 && touch src/main.rs && cargo build" \
--command-name "serde_json" \
"pushd tests/crates/stress2 && touch src/main.rs && cargo build"
Benchmark 1: typed_json
Time (mean ± σ): 157.5 ms ± 6.1 ms [User: 147.9 ms, System: 83.5 ms]
Range (min … max): 152.1 ms … 178.4 ms 18 runs
Benchmark 2: serde_json
Time (mean ± σ): 151.7 ms ± 4.5 ms [User: 133.6 ms, System: 97.9 ms]
Range (min … max): 145.1 ms … 162.4 ms 18 runs
Summary
serde_json ran
1.04 ± 0.05 times faster than typed_json
Release
$ hyperfine \
--command-name "typed_json" \
"pushd tests/crates/stress1 && touch src/main.rs && cargo build --release" \
--command-name "serde_json" \
"pushd tests/crates/stress2 && touch src/main.rs && cargo build --release"
Benchmark 1: typed_json
Time (mean ± σ): 1.501 s ± 0.012 s [User: 2.324 s, System: 0.090 s]
Range (min … max): 1.480 s … 1.520 s 10 runs
Benchmark 2: serde_json
Time (mean ± σ): 947.3 ms ± 20.4 ms [User: 1142.0 ms, System: 71.2 ms]
Range (min … max): 918.7 ms … 989.0 ms 10 runs
Summary
serde_json ran
1.58 ± 0.04 times faster than typed_json
Conclusion
I don't think I can conclusively say that typed-json introduces a compile-time regression in standard use.
At the extremes, it likely will need to compile many more types but in standard use, it can re-use a lot of prior compilations.